/* * Copyright (C) 2013 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.tools.lint.checks; import static com.android.SdkConstants.CONSTRUCTOR_NAME; import static com.android.tools.lint.checks.SecureRandomDetector.OWNER_SECURE_RANDOM; import com.android.annotations.NonNull; import com.android.annotations.Nullable; import com.android.annotations.VisibleForTesting; import com.android.tools.lint.detector.api.Category; import com.android.tools.lint.detector.api.ClassContext; import com.android.tools.lint.detector.api.Context; import com.android.tools.lint.detector.api.Detector; import com.android.tools.lint.detector.api.Detector.ClassScanner; import com.android.tools.lint.detector.api.Implementation; import com.android.tools.lint.detector.api.Issue; import com.android.tools.lint.detector.api.LintUtils; import com.android.tools.lint.detector.api.Location; import com.android.tools.lint.detector.api.Scope; import com.android.tools.lint.detector.api.Severity; import org.objectweb.asm.Opcodes; import org.objectweb.asm.tree.AbstractInsnNode; import org.objectweb.asm.tree.ClassNode; import org.objectweb.asm.tree.LdcInsnNode; import org.objectweb.asm.tree.MethodInsnNode; import org.objectweb.asm.tree.MethodNode; import java.util.Arrays; import java.util.Collections; import java.util.List; /** * Checks for pseudo random number generator initialization issues */ public class SecureRandomGeneratorDetector extends Detector implements ClassScanner { @SuppressWarnings("SpellCheckingInspection") private static final String BLOG_URL = "https://android-developers.blogspot.com/2013/08/some-securerandom-thoughts.html"; /** Whether the random number generator is initialized correctly */ public static final Issue ISSUE = Issue.create( "TrulyRandom", //$NON-NLS-1$ "Weak RNG", "Key generation, signing, encryption, and random number generation may not " + "receive cryptographically strong values due to improper initialization of " + "the underlying PRNG on Android 4.3 and below.\n" + "\n" + "If your application relies on cryptographically secure random number generation " + "you should apply the workaround described in " + BLOG_URL + " .\n" + "\n" + "This lint rule is mostly informational; it does not accurately detect whether " + "cryptographically secure RNG is required, or whether the workaround has already " + "been applied. After reading the blog entry and updating your code if necessary, " + "you can disable this lint issue.", Category.SECURITY, 9, Severity.WARNING, new Implementation( SecureRandomGeneratorDetector.class, Scope.CLASS_FILE_SCOPE)) .addMoreInfo(BLOG_URL); private static final String WRAP = "wrap"; //$NON-NLS-1$ private static final String UNWRAP = "unwrap"; //$NON-NLS-1$ private static final String INIT = "init"; //$NON-NLS-1$ private static final String INIT_SIGN = "initSign"; //$NON-NLS-1$ private static final String GET_INSTANCE = "getInstance"; //$NON-NLS-1$ private static final String FOR_NAME = "forName"; //$NON-NLS-1$ private static final String JAVA_LANG_CLASS = "java/lang/Class"; //$NON-NLS-1$ private static final String JAVAX_CRYPTO_KEY_GENERATOR = "javax/crypto/KeyGenerator"; private static final String JAVAX_CRYPTO_KEY_AGREEMENT = "javax/crypto/KeyAgreement"; private static final String JAVA_SECURITY_KEY_PAIR_GENERATOR = "java/security/KeyPairGenerator"; private static final String JAVAX_CRYPTO_SIGNATURE = "javax/crypto/Signature"; private static final String JAVAX_CRYPTO_CIPHER = "javax/crypto/Cipher"; private static final String JAVAX_NET_SSL_SSLENGINE = "javax/net/ssl/SSLEngine"; /** Constructs a new {@link SecureRandomGeneratorDetector} */ public SecureRandomGeneratorDetector() { } // ---- Implements ClassScanner ---- @Nullable @Override public List<String> getApplicableCallOwners() { return Arrays.asList( JAVAX_CRYPTO_KEY_GENERATOR, JAVA_SECURITY_KEY_PAIR_GENERATOR, JAVAX_CRYPTO_KEY_AGREEMENT, OWNER_SECURE_RANDOM, JAVAX_NET_SSL_SSLENGINE, JAVAX_CRYPTO_SIGNATURE, JAVAX_CRYPTO_CIPHER ); } @Nullable @Override public List<String> getApplicableCallNames() { return Collections.singletonList(FOR_NAME); } /** Location of first call to key generator (etc), if any */ private Location mLocation; /** Whether the issue should be ignored (because we have a workaround, or because * we're only targeting correct implementations, etc */ private boolean mIgnore; @Override public void checkCall(@NonNull ClassContext context, @NonNull ClassNode classNode, @NonNull MethodNode method, @NonNull MethodInsnNode call) { if (mIgnore) { return; } String owner = call.owner; String name = call.name; // Look for the workaround code: if we see a Class.forName on the harmony NativeCrypto, // we'll consider that a sign. if (name.equals(FOR_NAME)) { if (call.getOpcode() != Opcodes.INVOKESTATIC || !owner.equals(JAVA_LANG_CLASS)) { return; } AbstractInsnNode prev = LintUtils.getPrevInstruction(call); if (prev instanceof LdcInsnNode) { Object cst = ((LdcInsnNode)prev).cst; //noinspection SpellCheckingInspection if (cst instanceof String && "org.apache.harmony.xnet.provider.jsse.NativeCrypto".equals(cst)) { mIgnore = true; } } return; } // Look for calls that probably require a properly initialized random number generator. assert owner.equals(JAVAX_CRYPTO_KEY_GENERATOR) || owner.equals(JAVA_SECURITY_KEY_PAIR_GENERATOR) || owner.equals(JAVAX_CRYPTO_KEY_AGREEMENT) || owner.equals(OWNER_SECURE_RANDOM) || owner.equals(JAVAX_CRYPTO_CIPHER) || owner.equals(JAVAX_CRYPTO_SIGNATURE) || owner.equals(JAVAX_NET_SSL_SSLENGINE) : owner; boolean warn = false; if (owner.equals(JAVAX_CRYPTO_SIGNATURE)) { warn = name.equals(INIT_SIGN); } else if (owner.equals(JAVAX_CRYPTO_CIPHER)) { if (name.equals(INIT)) { int arity = getDescArity(call.desc); AbstractInsnNode node = call; for (int i = 0; i < arity; i++) { node = LintUtils.getPrevInstruction(node); if (node == null) { break; } } if (node != null) { int opcode = node.getOpcode(); if (opcode == Opcodes.ICONST_3 || // Cipher.WRAP_MODE opcode == Opcodes.ICONST_1) { // Cipher.ENCRYPT_MODE warn = true; } } } } else if (name.equals(GET_INSTANCE) || name.equals(CONSTRUCTOR_NAME) || name.equals(WRAP) || name.equals(UNWRAP)) { // For SSLEngine warn = true; } if (warn) { if (mLocation != null) { return; } if (context.getMainProject().getMinSdk() > 18) { // Fix no longer needed mIgnore = true; return; } if (context.getDriver().isSuppressed(ISSUE, classNode, method, call)) { mIgnore = true; } else { mLocation = context.getLocation(call); } } } @VisibleForTesting static int getDescArity(String desc) { int arity = 0; // For example, (ILjava/security/Key;)V => 2 for (int i = 1, max = desc.length(); i < max; i++) { char c = desc.charAt(i); if (c == ')') { break; } else if (c == 'L') { arity++; i = desc.indexOf(';', i); assert i != -1 : desc; } else { arity++; } } return arity; } @Override public void afterCheckProject(@NonNull Context context) { if (mLocation != null && !mIgnore) { String message = "Potentially insecure random numbers on Android 4.3 and older. " + "Read " + BLOG_URL + " for more info."; context.report(ISSUE, mLocation, message); } } }